require 'time'
require 'complex'

class Movie
  attr_reader :name
  attr_reader :showtime
  def initialize(name,showtime)
    @name = name
    @showtime = showtime
  end
end

movies = []
movies << Movie.new("Orphan", Time.parse("1:00 pm"))
movies << Movie.new("Orphan", Time.parse("4:05 pm"))
movies << Movie.new("Orphan", Time.parse("7:00 pm"))
movies << Movie.new("GI Joe", Time.parse("1:05 pm"))
movies << Movie.new("GI Joe", Time.parse("4:30 pm"))
movies << Movie.new("GI Joe", Time.parse("6:50 pm"))
movies << Movie.new("Rocky Horror", Time.parse("12:00 am"))
movies << Movie.new("District 9", Time.parse("11:00 am"))
movies << Movie.new("District 9", Time.parse("3:00 pm"))

################################################################################################
#######	Basic OOP, Ruby libraries, introspection #######
################################################################################################

#define a method called squared that returns the square of its receiver, and add it to the Numeric class.
class Numeric
	def squared 
		return  self * self if self.imag == 0
		
		real_num = (self.real * self.real) + (self.imag * self.imag)
		complex_num = 2 * (self.real * self.imag)
		return Complex(real_num,complex_num)
	end
end

#**********************************************************************

=begin
Why must we say Time.parse("1:00 pm") but we can't say "1:00 pm".parse ? 
We cannot use "1:00 pm".parse because "1:00 pm" will be recognized as a String object not Time object.
That is why we have to use Time.parse("1:00 pm")
To use the "1:00 pm".parse syntax, we need to add our own formats to the Time::DATE_FORMATS hash. 
Use the format name as the hash key and either a strftime string or Proc instance that takes a time argument as the value. 
=end

#**********************************************************************

#Modify the initialize method of the Movie class so that if the showtime argument passed in is a Time object (or any subclass of Time), it's used intact to set the @showtime instance variable; 
#if a string, it's parsed and converted into a Time object (as we did in the example code above); otherwise, an ArgumentError exception is raised.
class Movie
	def initialize(name,showtime)
		@name = name
		
		if showtime.kind_of?(Time) then @showtime = showtime 
		elsif showtime.instance_of?(String) then @showtime = Time.parse(showtime)
		else raise ArgumentError.new("Only Time or String are allowed for showtime")
		end
	end
end

#**********************************************************************

#Define a convenience method name_and_showtime that returns a string containing the movie's name and showtime
#If the movie's showtime is later than right now, it should output "next show at"; if it's earlier than or equal to now, it should output "already started at".
class Movie
	def name_and_showtime
		movie_time = @showtime.strftime("%I:%M %p")
		if Time.now < @showtime then puts "#@name : next show at #{@showtime.strftime("%I:%M %p")}"
		else puts "#@name : already started at #{@showtime.strftime("%I:%M %p")}"
		end
	end
end

################################################################################################
#######	Modules #######
################################################################################################

#Arrange for it to be the case that a comparison such as movie1 <=> movie2 returns -1, 0 or 1 based on whether movie1's showtime is earlier, 
#the same or later than movie2's, by adding the <=> method to the Movie class.
class Movie
	include Comparable
  
	def <=>(another_movie)
		@showtime.to_f <=> another_movie.showtime.to_f
	end
end

#**********************************************************************
#Mix the Comparable module into the Movie class, and verify that movie1.between?(movie2,movie3) returns true iff movie1's starting time is later than movie2's and earlier than movie3's, 
#and also that the operators <, >, <=, >= and == now work too.

movie1 = Movie.new("Harry Potter",Time.parse("5:00 pm"))
movie2 = Movie.new("Rambo","10:00 pm")
movie3 = Movie.new("Terminator", "9:00 pm")
movie4 = Movie.new("Doraemon","1:00 pm")
movie5 = Movie.new("Terminator", "9:00 pm")

=begin
puts movie1.between?(movie4,movie5)
puts (movie1 > movie5)
puts (movie3 < movie2)
puts (movie3 == movie5)
=end

#**********************************************************************

#Briefly explain why it works even though you never defined a sort() method in Movie.  
#In other words: where is the sort() method actually defined, why does it work on Movies if you didn't define it in the Movie class, and how does it know how to compare two Movie objects for sort ordering?)

=begin
We do not need to implement "sort" because "sort" is part of an array method.
Since "sort" is done using the <=> operator and "Movie" class already support <=> operation, we can just use sort
=end

################################################################################################
#######	Higher-order functions & functional programming #######
################################################################################################
module Enumerable
	def group_by
		possible_outputs = []
		corresponding_values=[]
		self.each do |element| 
			output = yield(element)
			if possible_outputs.include?(output) then corresponding_values[possible_outputs.index(output)].push(element)
			else 
				possible_outputs.push(output)
				corresponding_values.push([element])				
			end
		end
		
		your_hash = Hash.new
		i=0
		
		possible_outputs.each do |key|
			your_hash[key]=corresponding_values[i]
			i=i+1
		end

		return your_hash
	end
end

#**********************************************************************

#Test that your method works on Movies by attempting to group the movies array by movie name. 
class Movie
	include Enumerable
end

#puts movies.group_by { |movie| movie.name }

#**********************************************************************

# write a single line of code that will return only those movie objects whose start time is later than 5:00 pm.
result = movies.select{|movie| movie.showtime>Time.parse("5:00 pm")}
#puts result[0].name

# write a single line of code that will return the number of movie objects that have a start time later than 5 PM.
result = movies.select{|movie| movie.showtime>Time.parse("5:00 pm")}.length
#puts result

# (trickier) write a single line of code that will return only the names of the movies that have a start time later than 5 PM, with no duplicates.
result = movies.select{|movie| movie.showtime>Time.parse("5:00 pm")}.collect{|movie| movie.name}.uniq
#puts result

################################################################################################
#######	Iterators #######
################################################################################################

module Enumerable
	def each_even
		self.each do |element|
			yield element if element.remainder(2)==0
		end
		return self
	end
end

#x = (1..9)
#x.each_even { |n| puts n }

#**********************************************************************
#Turn your group_by method above into an iterator called each_grouped_by_first_letter. 
#The iterator should yield two items at a time: a one-letter string and an array of the input words starting with that letter

class String 
	def first_letter  
		self[0,1].to_s ; 
	end
end

module Enumerable
	def each_grouped_by_first_letter
		group_hash = self.group_by{|x| x.first_letter}
		
		group_hash.each_pair do |key,value|
			yield(key,value)
		end
	end
end

################################################################################################
#######	Extending classes #######
################################################################################################

#Suppose we wanted to extend this to hashes, so that if "foo" is a key of hash h, then h["foo"] and h[:foo] would be equivalent.
#Create a class called IndifferentHash that has this behavior. Make it a subclass of Hash so that it can be used anywhere a Hash would be used.
class IndifferentHash < Hash 
	def initialize
		super
	end
	
	def [](key)
		if key.is_a?(Symbol) then super(key.to_s)
		else super(key)
		end
	end
	
	def []=(key,value)
		if key.is_a?(Symbol) then super(key.to_s,value)
		else super(key,value)
		end
	end
end

=begin
#TEST CASES
myHash = IndifferentHash.new
myHash["mumu"]="nita"
puts myHash["mumu"]
myHash[:lexie]="prisil"
puts myHash[:lexie]
=end

################################################################################################
#######	Language extensions for convenience #######
################################################################################################

#Verify that setting a variable x to the empty string (e.g. x="") causes true to be returned for x.empty?, but that setting the variable to nil or a string of whitespaces is problematic.
=begin
x = ""
puts x.empty?
#Calling 'empty?' to empty string will return True

y.empty?
#Calling 'empty?' to undefined variable cause: Undefined local variable or method 'y' for main:Object (NameError)

z=nil
puts z.empty?
#Calling 'empty?' to nill cause error: Undefined method 'empty' for nil:NilClass (No Method Error)

x = " "
puts x.empty?
#Calling 'empty?' to string of whitespace return false. It is ambiguous because a string of whitespace can also be considered as an empty string
=end

#**********************************************************************

#Define a method blank? that can be called on any receiver. blank? should return true if: 1) empty? would return true; 2) the receiver is nil.
module MyModule
	def blank?
		if (self.respond_to?(:empty?) && self.empty?) then return true
		elsif self.nil? then return true
		else return false
		end
	end
end

#**********************************************************************

#Show how to add your new method to the String class so that all strings can respond to blank?
class NilClass
	include MyModule
end

class String
	include MyModule
	#def nil?
	#end
end

=begin
a=""
puts a.blank?
a=nil
puts a.blank?
a=" "
puts a.blank?
=end

#**********************************************************************
module MyModule
	def blank?
		if self.respond_to?(:empty?) then return self.empty?
		elsif self.nil? then return true
		else return false
		end
	end
end

class Object
	include MyModule
end

=begin
a=""
puts a.blank?
a=nil
puts a.blank?
a=" "
puts a.blank?
a=[]
puts a.blank?
a={}
puts a.blank?
=end
